using System;
using System.IO;
using System.Threading;
using System.Xml;
using System.Xml.Serialization;
using System.Collections.Generic;

using Dashboard;
using Dashboard.Util;
using Dashboard.Engine;

using Beagle;

namespace Dashboard.Engine.Backends
{
	public class BeagleBackend : Backend, IComponent
	{
		private string rules_directory;
		private string mappings_directory;
		
		private List<Rule> rules = new List<Rule> ();
		private List<Mapping> mappings = new List<Mapping> ();

		private bool debug_lookup = false;

		public void Initialize ()
		{
			if (Environment.GetEnvironmentVariable ("DASHBOARD_DEBUG_LOOKUP") != null)
				debug_lookup = true;

 			// Setup and check the rules and mappings 
			// directories.

			rules_directory = Environment.GetEnvironmentVariable ("DASHBOARD_RULES_DIRECTORY");

			if (String.IsNullOrEmpty (rules_directory))
				rules_directory = Path.Combine (PathFinder.Installation, "Dashboard.Engine.Rules");

			mappings_directory = Environment.GetEnvironmentVariable ("DASHBOARD_MAPPINGS_DIRECTORY");

			if (String.IsNullOrEmpty (mappings_directory))
				mappings_directory = Path.Combine (PathFinder.Installation, "Dashboard.Engine.Mappings");

			// We're essentially useless without our list
			// of rules and/or mappings - so bail out if not found.
			
			if (!Directory.Exists (rules_directory) || !Directory.Exists (mappings_directory)) {
				Engine.Log.Error ("Cannot find directory --- bailing out!");
				return;
			}
			
			// Load and initialize our search rules.
			
			foreach (string rule_list_file in Directory.GetFiles (rules_directory)) {
				RuleList rule_list;
				
				try {
					rule_list = RuleList.Load (rule_list_file);
				} catch (XmlException) {
					Engine.Log.Warn ("Malformed data in RuleList: {0}", Path.GetFileName (rule_list_file));
					continue;
				}
				
				rules.AddRange (rule_list);
			}

			Engine.Log.Debug ("Loaded {0} rules", rules.Count);

			// Load and initialize our match mappings.

			foreach (string mapping_list_file in Directory.GetFiles (mappings_directory)) {
				MappingList mapping_list;
				
				try {
					mapping_list = MappingList.Load (mapping_list_file);
				} catch (XmlException) {
					Engine.Log.Warn ("Malformed data in MappingList: {0}", Path.GetFileName (mapping_list_file));
					continue;
				}
				
				mappings.AddRange (mapping_list);
			}

			Engine.Log.Debug ("Loaded {0} mappings", mappings.Count);

			// Finally, subscribe to clue messages and let
			// the magic happen.
			
			Engine.MessageBroker.Subscribe ("/event/*", typeof(ClueMessage), OnMessage);
		}

		public void Dispose ()
		{
			Engine.MessageBroker.Unsubscribe (OnMessage);

			rules.Clear ();
			mappings.Clear ();
		}
		
		private void OnMessage (Message message)
		{
			Engine.Log.Debug ("Processing message");

			ClueMessage clue_message = message as ClueMessage;

			// Fire off a bunch of queries - cross your
			// fingers.

			foreach (QueryRule query_rule in MatchRules (clue_message)) {
				QueryHandler handler = new QueryHandler (this, clue_message, query_rule);
				handler.Start ();
			}
		}
		
		private List<QueryRule> MatchRules (ClueMessage clue_message)
		{
			List<QueryRule> matched_query_rules = new List<QueryRule> ();

			foreach (Rule rule in rules) {
				if (!rule.Match (clue_message))
					continue;

				foreach (QueryRule query_rule in rule.Queries) {
					if (query_rule.Match (clue_message))
						matched_query_rules.Add (query_rule);
				}
			}
			
			if (debug_lookup) {
				Engine.Log.Debug ("Matched {0} query rules against '{1}'", matched_query_rules.Count, clue_message.Path);
				
				foreach (QueryRule query_rule in matched_query_rules)
					Engine.Log.Debug ("Query rule: {0}", query_rule.Description);
			}
			
			return matched_query_rules;
		}

		private List<Mapping> MatchMappings (Hit hit)
		{
			List<Mapping> matched_mappings = new List<Mapping> ();

			foreach (Mapping mapping in mappings) {
				if (mapping.Match (hit))
					matched_mappings.Add (mapping);
			}

			return matched_mappings;
		}

		private class QueryHandler
		{
			private BeagleBackend backend = null;
			private ClueMessage clue_message = null;
			private QueryRule query_rule = null;

			private Thread thread = null;
			private Query query = null;

			private static bool debug_lookup = false;

			static QueryHandler ()
			{
				if (Environment.GetEnvironmentVariable ("DASHBOARD_DEBUG_LOOKUP") != null)
					debug_lookup = true;
			}

			public QueryHandler (BeagleBackend backend, ClueMessage clue_message, QueryRule query_rule)
			{
				this.backend = backend;
				this.clue_message = clue_message;
				this.query_rule = query_rule;
			}

			public void Start ()
			{
				query = new Query ();
				query.HitsAddedEvent += OnHitsAdded;
				query.FinishedEvent += OnFinished;				
				query.Keepalive = false;

				// Limit query on required hit types
				foreach (string type in query_rule.Types) {
					QueryPart_Property part = new QueryPart_Property ();
					part.Type = PropertyType.Keyword;
					part.Key = "beagle:HitType";
					part.Value = type;
					query.AddPart (part);
				}
				
				foreach (QueryRuleProperty query_rule_property in query_rule.Properties) {

					Clue clue = clue_message.Clues [query_rule_property.Clue];
					if (clue == null || clue.Properties [query_rule_property.Property] == null)
						continue;

					List<string> property_values = new List<string> ();

					if (String.IsNullOrEmpty (query_rule_property.Value)) {

						// Gather up multiple values for the specific
						// property if the Multiple flag is set.

						if (String.IsNullOrEmpty (query_rule_property.Clue) ||
							String.IsNullOrEmpty (query_rule_property.Property))
							throw new ApplicationException ("clue and/or property is missing");
						
						if (query_rule_property.Multiple) {
							foreach (Property property in clue.Properties.GetProperties (query_rule_property.Property))
								property_values.Add (property.Value);
						} else {
							property_values.Add (clue.Properties [query_rule_property.Property].Value);
						}
					} else {

						// If the QueryRuleProperty.Value is set, just
						// add the value to the list (and dont care about
						// any other things).

						if (!String.IsNullOrEmpty (query_rule_property.Clue) ||
							!String.IsNullOrEmpty (query_rule_property.Property))
							throw new ApplicationException ("clue and property is not compatible when value is specifies");
						
						property_values.Add (query_rule_property.Value);
					}
					
					// If there is no target specified for our
					// QueryRuleProperty we consider it to be a 
					// free-for-all search.
					
					if (String.IsNullOrEmpty (query_rule_property.Target)) {

						if (query_rule_property.Type == QueryRulePropertyType.Prohibited)
							throw new ApplicationException ("prohibited properties not supported on target-less rules");
						
						QueryPart_Or query_part_block = new QueryPart_Or ();

						// FIXME: This is a pretty shady way of doing queries
						// based on a block of text. Since we now per default
						// do AND queries in Beagle we cannot use all the nice
						// features for doing this kind of stuff in Lucene. We 
						// might want to consider adding a QueryPart for this
						// into Beagle. And oh, otherwise we should have a real
						// tokenxw analyzer or something.
												
						foreach (string property_value in property_values) {
							foreach (string token in property_value.Split (' ')) {
								QueryPart_Text query_part_text = new QueryPart_Text ();
								query_part_text.Text = token;
								query_part_block.Add (query_part_text);
							}
						}

						// FIXME: We dont care if the type is set to Required
						// or Optional since this is a target-less query,
						// so just add it to our main query for the time
						// being.

						query.AddPart (query_part_block);

					} else {

						// Construct a keyword query for the specified
						// target property (for a list of property values
						// in the case where the Multiple flag is set).

						foreach (string property_value in property_values) {
							QueryPart_Property query_part = new QueryPart_Property ();

							switch (query_rule_property.TargetType) {
							case QueryRulePropertyTargetType.Keyword:
								query_part.Type = PropertyType.Keyword;
								break;
							case QueryRulePropertyTargetType.Date:
								query_part.Type = PropertyType.Date;
								break;
							default:
								query_part.Type = PropertyType.Text;
								break;
							}

							query_part.Key = query_rule_property.Target;
							query_part.Value = property_value;

							// Determine the requirement of the property
							// and set the query-part as requested.
						
							switch (query_rule_property.Type) {
							case QueryRulePropertyType.Required:
								query.AddPart (query_part);
								break;
							case QueryRulePropertyType.Optional:
								// FIXME: Implement optional property-queries.
								break;
							case QueryRulePropertyType.Prohibited:
								query_part.Logic = QueryPartLogic.Prohibited;
								query.AddPart (query_part);
								break;
							}
						}
					}					
				}

				thread = new Thread (new ThreadStart (Worker));
				thread.Start ();
			}
			
			private void Worker ()
			{
				if (debug_lookup)
					Engine.Log.Debug ("Executing QueryHandler for rule: {0}", query_rule.Description);

				try {
					query.SendAsyncBlocking ();
				} catch (Exception ex) {
					Engine.Log.Error ("Caught exception while executing query: {0}\n{1}", query_rule.Description, ex);
				}
			}

			private void OnHitsAdded (HitsAddedResponse response)
			{
				if (debug_lookup)
					Engine.Log.Debug ("Got hits for rule: {0}", query_rule.Description);

				MatchMessage match_message = null;
				ClueMessage clue_message = null;

				foreach (Hit hit in response.Hits) {

					// FIXME: Implement hit capping based on score,
					// configurable in the query rules.

					// FIXME: Having multiple matched mappings per 
					// hit might not prove that useful afterall. We
					// should probably weigh them or something and 
					// just use the highest ranked mapping.
					
					foreach (Mapping mapping in backend.MatchMappings (hit)) {
						List<Property> properties = new List<Property> ();
						
						foreach (MappingProperty mapping_property in mapping.Properties) {
							if (mapping_property.Multiple) {
								string[] property_values = hit.GetProperties (mapping_property.Source);								
								if (property_values == null)
									continue;
								
								foreach (string property_value in property_values) {
									if (!String.IsNullOrEmpty (mapping_property.Pattern) &&
									    !StringFu.GlobMatch (mapping_property.Pattern, property_value))
										continue;
									
									properties.Add (new Property (mapping_property.Target, property_value));
								}
							} else {
								string property_value = hit.GetFirstProperty (mapping_property.Source);
								if (property_value == null)
									continue;
								
								if (!String.IsNullOrEmpty (mapping_property.Pattern) &&
								    !StringFu.GlobMatch (mapping_property.Pattern, property_value))
									continue;
								
								properties.Add (new Property (mapping_property.Target, property_value));
							}
						}
						
						// If the query rule specifies the mapped type as
						// a potential clue - construct a new clue and append
						// it to our clue message.

						if (query_rule.Clues.Contains (mapping.Type)) {
							if (clue_message == null)
								clue_message = this.clue_message.Clone () as ClueMessage;

							Clue clue = new Clue ();
							clue.Type = mapping.Type;
							clue.Properties.AddRange (properties);
						  
							clue_message.Clues.Add (clue);
						}

						// Construct a match. This is done without regard
						// to if we found the hit to be a suitable new clue
						// or not. Sometimes this might not be the wanted 
						// behaviour though, so we probably want to add a rule
						// property for that.

						if (match_message == null) {
							match_message = new MatchMessage ();
							match_message.Relation = query_rule.Relation;
						}

						Match match = new Match ();
						match.Uri = hit.Uri;

						match.Type = mapping.Type;
						match.Trigger = this.clue_message.Id;
						match.Relevancy = (hit.Score * query_rule.Relevancy);

						match.Timestamp = hit.Timestamp;

						match.Properties.AddRange (properties);

						match_message.Matches.Add (match);
					}
				}

				if (match_message != null)
					Engine.MessageBroker.Send (match_message);

				// Only re-inject our clue_message into the engine
				// if we've found any new clues to go with it.

				if (clue_message != null)
					Engine.MessageBroker.Send (clue_message);
			}

			private void OnFinished (FinishedResponse response)
			{
				if (debug_lookup)
					Engine.Log.Debug ("QueryHandler Finished for rule: {0}", query_rule.Description);

				// FIXME: Clean-up (my apartment).
			
				query.Close ();
			}
		}

		public class Rule
		{
			// A list of glob patterns to match the paths
			// of the clue messages against.
			[XmlArray]
			[XmlArrayItem (ElementName="Pattern")]
			public List<string> Patterns = new List<string> ();

			[XmlArray]
			[XmlArrayItem (ElementName="Query")]
			public List<QueryRule> Queries = new List<QueryRule> ();

			public bool Match (ClueMessage clue_message)
			{
				foreach (string pattern in Patterns) {
					if (StringFu.GlobMatch (pattern, clue_message.Path))
						return true;
				}
				
				return false;
			}
		}

		public class QueryRule 
		{
			// A short description of what the query rule does,
			// for debugging purposes.
			[XmlAttribute]
			public string Description;

			// Describes the relation between the clue and
			// the resulting match. If the query rule finds pictures
			// taken the same day, it might be "related-date", or if
			// it looks for similar documents, it'll be "related-content".
			// FIXME: We need to specify these and make a list of them.
			[XmlAttribute]
			public string Relation;

			// A relevancy multiplier for the Beagle score of the match.
			[XmlAttribute]
			public double Relevancy = 1;

			// List of Beagle types that are allowed.
			[XmlArray]
			[XmlArrayItem (ElementName="Type")]
			public List<string> Types = new List<string> ();

			// List of possible mapping types that we should map
			// as a new clue (cluechaining) besides mapping the
			// hit as a new match.
			[XmlArray]
			[XmlArrayItem (ElementName="Clue")]
			public List<string> Clues = new List<string> ();

			// List of prerequisites for the query to execute. 
			// FIXME: This is kinda ugly and slower than need be, 
			// but I kept it separate from the Properties for 
			// peace of mind, for now.
			[XmlArray]
			[XmlArrayItem (ElementName="Prerequisite")]
			public List<QueryRulePrerequisite> Prerequisites = new List<QueryRulePrerequisite> ();

			// List of all the properties (clue/hit) that we're 
			// searching for. Look at the class definition of 
			// QueryRuleProperty for a closer look.
			[XmlArray]
			[XmlArrayItem (ElementName="Property")]
			public List<QueryRuleProperty> Properties = new List<QueryRuleProperty> ();

			public bool Match (ClueMessage clue_message)
			{
				Clue clue;
				foreach (QueryRulePrerequisite query_rule_prerequisite in Prerequisites) {
					if (String.IsNullOrEmpty (query_rule_prerequisite.Clue))
						throw new ApplicationException ("clue name required in query rule prerequisite");

					// FIXME: Kill the swedish spaghetti chef.

					clue = clue_message.Clues [query_rule_prerequisite.Clue];
					if (clue == null) {
						if (query_rule_prerequisite.Type == QueryRulePrerequisiteType.Required)
							return false;
						else continue;
					} else 	if (String.IsNullOrEmpty (query_rule_prerequisite.Property)) {
						if (query_rule_prerequisite.Type == QueryRulePrerequisiteType.Prohibited)
							return false;
						else continue;
					} else  if (clue.Properties.Contains (query_rule_prerequisite.Property)) {
						if (query_rule_prerequisite.Type == QueryRulePrerequisiteType.Prohibited)
							return false;
						else continue;
					} else if (query_rule_prerequisite.Type == QueryRulePrerequisiteType.Required) {
						return false;
					}
				}

				return true;
			}
		}

		public class QueryRulePrerequisite
		{
			// Name of the clue, required to be set.
			[XmlAttribute]
			public string Clue;

			// Name of the clue property, optional.
			[XmlAttribute]
			public string Property;

			// Glob pattern to match the value of the clue property,
			// if the clue property name is set.
			[XmlAttribute]
			public string Pattern;

			// Type of prerequsite, the clue/property pair is either 
			// required or prohibited.
			[XmlAttribute]
			public QueryRulePrerequisiteType Type = QueryRulePrerequisiteType.Required;
		}

		public enum QueryRulePrerequisiteType
		{
			Required,
			Prohibited
		}

		public class QueryRuleProperty
		{
			// Name of the clue
			[XmlAttribute]
			public string Clue;

			// Name of the clue property
			[XmlAttribute]
			public string Property;

			[XmlAttribute]
			public string Value;

			// Specifies if we should process all of the
			// properties with the same name in the clue
			// or just the first, single, one.
			[XmlAttribute]
			public bool Multiple = false;

			[XmlAttribute]
			public QueryRulePropertyType Type = QueryRulePropertyType.Optional;

			// Name of the Beagle property to search
			// against.
			[XmlAttribute]
			public string Target;

			// Type of the Beagle property. This controls 
			// how the Beagle daemon processes the property 
			// (clue) value, and also depends of how the 
			// property is indexed in the first place. If 
			// it's set to Keyword instead of Text, Beagle
			// will match it -exactly-.
			[XmlAttribute]
			public QueryRulePropertyTargetType TargetType = QueryRulePropertyTargetType.Text;

			// FIXME: Implement property based relevancy multipliers,
			// we will need some changes made to Beagle for this.

			//[XmlAttribute]
			//public double Relevancy;
		}

		public enum QueryRulePropertyType
		{
			Optional,
			Required,
			Prohibited
		}

		public enum QueryRulePropertyTargetType
		{
			Keyword,
			Date,
			Text
		}

		[XmlRoot (ElementName = "Rules")]
		public class RuleList : List <Rule>
		{
			private static XmlSerializer serializer = new XmlSerializer (typeof (RuleList));
			
			public static RuleList Load (string path)
			{
				return serializer.Deserialize (File.OpenRead (path)) as RuleList;
			}
		}
		
		public class Mapping
		{
			// Type of match (or clue, if we're doing
			// cluechaining) to map this hit to.
			[XmlAttribute]
			public string Type;

			// Types of Beagle hits to match this to.
			[XmlArray]
			[XmlArrayItem (ElementName="Type")]
			public List<string> Types = new List<string> ();

			// List of prerequisites for the query to execute. 
			// FIXME: This is kinda ugly and slower than need be, 
			// but I kept it separate from the Properties for 
			// peace of mind, for now.
			[XmlArray]
			[XmlArrayItem (ElementName="Prerequisite")]
			public List<MappingPrerequisite> Prerequisites = new List<MappingPrerequisite> ();

			// List of properties to match from/to.
			[XmlArray]
			[XmlArrayItem (ElementName="Property")]				
			public List<MappingProperty> Properties = new List<MappingProperty> ();

			public bool Match (Hit hit)
			{
				if (!Types.Contains (hit.Type))
					return false;
				
				foreach (MappingPrerequisite prerequisite in Prerequisites) {
					if (String.IsNullOrEmpty (prerequisite.Property))
						throw new ApplicationException ("property name missing in mapping prerequisite");

					string[] property_values = hit.GetProperties (prerequisite.Property);
					if (property_values == null)
						return false;

					if (String.IsNullOrEmpty (prerequisite.Pattern))
						return true;

					foreach (string property_value in property_values) {
						if (StringFu.GlobMatch (prerequisite.Pattern, property_value) &&
							prerequisite.Type == MappingPrerequisiteType.Single)
							return true;

						if (!StringFu.GlobMatch (prerequisite.Pattern, property_value) &&
							prerequisite.Type == MappingPrerequisiteType.Every)
							return false;
					}
				}	

				return true;
			}
		}

		public class MappingPrerequisite
		{
			// Name of the Beagle property
			[XmlAttribute]
			public string Property;

			// Glob pattern to match the value of the
			// Beagle property against to be a success.
			[XmlAttribute]
			public string Pattern;

			// Specifies if the pattern (is specified)
			// must apply to a single, or every, property
			// in the hit.
			[XmlAttribute]
			public MappingPrerequisiteType Type = MappingPrerequisiteType.Single;
		}

		public enum MappingPrerequisiteType
		{
			Every,
			Single
		}

		public class MappingProperty
		{
			// Name of the property on the Beagle side
			[XmlAttribute]
			public string Source;

			// Name of the property on the Dashboard side.
			[XmlAttribute]
			public string Target;

			// Specifies if the property is allowed
			// multiple times. If not - we'll just grab
			// the first property matching the name and
			// discard any other.
			[XmlAttribute]
			public bool Multiple = true;

			// A glob pattern to match the value of the
			// property against. If empty, we'll accept 
			// anything.
			[XmlAttribute]
			public string Pattern;
		}

		[XmlRoot (ElementName = "Mappings")]
		public class MappingList : List <Mapping>
		{
			private static XmlSerializer serializer = new XmlSerializer (typeof (MappingList));
			
			public static MappingList Load (string path)
			{
				return serializer.Deserialize (File.OpenRead (path)) as MappingList;
			}
		}

		public string Name
		{
			get { return "Beagle"; }
		}

		public string Description
		{
			get { return ""; }
		}

		public List<Mapping> Mappings 
		{
			get { return mappings; }
		}

		public List<Rule> Rules
		{
			get { return rules; }
		}
	}
}
